/*************************************************************
用例：
0.连线版/离线版 在系统上电后10秒，串口有一次机会选择离线版还是连线版，离线版将不做blynk连接。连线版(默认)将永远尝试blynk连接。
  wifi问题
1.用户将产品转移到新连接环境，产品无法连接上网，产品串口提示无法连接wifi,问是否变更wifi设置。否（默认），继续连接。是，变更设置再连接。
2.用户路由器没有插电，产品能在串口提示无法连接wifi，问是否变更wifi设置。否（默认），继续连接。是，变更设置再连接。
  blynk问题
3.区分小区断网和blynk服务器地址密码等错误。前者继续连接，后者提示修改。否（默认），继续连接。是，变更设置再连接。
  小区断网。ping 8.8.8.8 不通，blynk服务器无法连接。转离线版。如需上线，按网络连接按钮。
  小区没断网。ping 8.8.8.8 通，blynk服务器无法连接。提示是否修改blynk服务器。否（默认），继续连接。是，变更设置再连接。
重要技术提示：T4S和T10S都是连接wifi路由器和blynk服务器需要等待的时间。本来在状态机中等待应该在对应的CON_WIFI_CHK和CON_BLYNK_CHK这两个状态等待。
但是在这两个状态下等待，就会无法响应用户的按键变更。所以我巧妙地把等待提前到了接收按键输入的CON_FIX_WIFI_PASS和CON_FIX_BLYNK_IP状态了。
备注:创新实验室小米 192.168.31.92:8080 T_G1cRBPNbYTnnwD5W2w4o94frhZh9ql
   ：家里         192.168.1.2  :8080 cYH0gGrufOalODWEaG-CYSsGbgqDNmJy
todo:
  1.在输入添加rst复位一切
 *************************************************************/
#ifndef USER_LOGIN_H
#define USER_LOGIN_H

#include <string.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <BlynkSimpleEsp32.h>
#include <EEPROM.h>

int DEBUG = 1; //调试开关
#define EEPROM_SIZE 256 //esp32最大可以有512字节
#define T4S  4000L
#define T10S 10000L         //定时查看连接的时间
enum conStateType {        //userLogin这个状态机的各个状态枚举
  CON_RST = 0,              //初始化
  CON_ONLINE_YN,                  //处理是否进行修改连接参数
  CON_FIX_GUID,             //确认修改连接参数后，根据userLogin.conInfo.conError进入相应修改状态
  CON_FIX_WIFI_PRINT,       //提示wifi输入
  CON_FIX_WIFI_SSID,        //修改wifi ssid
  CON_FIX_WIFI_PASS,        //修改wifi pass
  CON_FIX_BLYNK_PRINT,      //提示blynk参数输入
  CON_FIX_BLYNK_IP,         //修改blynk ip
  CON_FIX_BLYNK_PORT,       //修改blynk port 端口
  CON_FIX_BLYNK_TOKEN,      //修改blynk token 令牌
  CON_WIFI_CHK,             //日常wifi检查
  CON_BLYNK_CHK             //日常blynk检查
};
//该结构的对象是要完整存储在EEPROM上面的，用char可以方便数出用了总共几个字节
struct ConnectInfo {
  // 长久保存的变量
  char conError;
  char auth[35];
  char ssid[25];
  char pass[25];
  char serverIP[20];
  char serverPort[10];
};

//这个结构体数据是保存临时使用的变量，但是为了方便组织和阅读，我把它们都放在了结构体里。因为这样总比散乱地写全局变量要好
struct UserLogin{
  // 中间变量工具
  char eeAddress;
  conStateType preConState; //debug用的上一个状态的枚举
  conStateType conState;  //用枚举的方式声明状态
  char wifiErrorCount;      //记录wifi重连失败次数
  unsigned long time0wifi;      //系统所有信息填写完成转入CON_WIFI_CHK状态的开始时间戳
  unsigned long time0blynk;     //系统所有信息填写完成转入CON_BLYNK_CHK状态的开始时间戳
  bool offlineFlag;              //true表示运行在离线模式
  bool infoInput;          /*从输入wifi名开始，系统认为用户需要测试新连接即infoInput=true，
                              直到blynk服务器的令牌输入好后,infoInput=false。这个变量的目的是在infoInput=true的时候
                              blynk T10S连接就可以跳过了。因为会影响用户体验*/
  char index;               //串口读信息是的字节索引
  char command[20];         //诸如修改参数命令fix等都放在这里
  struct ConnectInfo oldConInfo; //先前连接用的连接数据结构体，这个用来跟最新更改的连接信息结构体比较用。
  // 长久保存的变量
  struct ConnectInfo conInfo; //需要长久在EEPROM保存的连接数据  
};
struct UserLogin userLogin;
void userLoginInit(){
  userLogin.eeAddress = 0;
  EEPROM.begin(EEPROM_SIZE); //使用EEPROM之前都是要初始化的
  EEPROM.get(userLogin.eeAddress, userLogin.conInfo); //从userLogin.eeAddress的地址的EEPROM上读取结构体数据给userLogin.conInfo结构体用
  memcpy(&userLogin.oldConInfo,&userLogin.conInfo,sizeof(userLogin.conInfo)); //更新老结构体，以备将来再用
  userLogin.conState = CON_RST;
  userLogin.conState = CON_RST;
  userLogin.offlineFlag = false;
  userLogin.infoInput = false; //刚开始的时候，默认不是处于系统连接参数输入阶段
  userLogin.index = 0;
  userLogin.time0wifi = 0;
  userLogin.wifiErrorCount = 0;  //只有在刚刚完成wifi参数输入的时候wifiErrorCount才是0
}
bool userLoginRun(){
  if(DEBUG)userLogin.preConState = userLogin.conState; //调试用
  switch(userLogin.conState)
  {
    case CON_RST: //复位
      userLogin.conState = CON_ONLINE_YN;
      Serial.print("本产品运行在在线模式吗y/n?\n");
    break;
    
    case CON_ONLINE_YN:
      if(Serial.available()){
        char c = Serial.read();
        userLogin.command[userLogin.index] = c;
        userLogin.index++;
        if( c == '\n') {
          c = '\0';
          userLogin.command[userLogin.index-1] = c;
          userLogin.index = 0;             //用完index后重新回0,为其他输入做准备
          if(!strcmp(userLogin.command,"y")||!strcmp(userLogin.command,"Y")){
            Serial.print("稍等..\n");
            userLogin.conState = CON_WIFI_CHK; //如果输入了y就进入到wifi检测状态，用来判断要wifi修改还是blynk修改
            userLogin.index = 0;   //每次要调到别的地方去了，我们有义务index清理
            userLogin.offlineFlag = false;
            break;
          }else if(!strcmp(userLogin.command,"n")||!strcmp(userLogin.command,"N")){
            userLogin.conState = CON_RST; //选择离线模式，那就永远在这个询问状态待着
            userLogin.index = 0;   //每次要调到别的地方去了，我们有义务index清理
            userLogin.offlineFlag = true;            
          }
        }        
      }else{
        if(millis() > T10S && userLogin.offlineFlag == false && millis() - userLogin.time0wifi >= T4S){
          userLogin.index = 0;
          userLogin.conState = CON_WIFI_CHK; //进入日常wifi检查模式
        }
      }
    break;
    
    case CON_FIX_GUID: //根据读出来的conError类型
      if(userLogin.conInfo.conError == 0) { //说明conError == 0 表示EEPROM内数据有效可上网 1 wifi 错误 2 blynk 连接错误
        if(memcmp(&userLogin.conInfo,&userLogin.oldConInfo,sizeof(userLogin.conInfo))){//比较新修改的结构体和原来的结构体信息，如果不同，那么向EEPROM中写入新的连接信息结构体
          Serial.print("写入新EEPROM\n");
          EEPROM.put(userLogin.eeAddress, userLogin.conInfo);
          EEPROM.commit();
        }
        memcpy(&userLogin.oldConInfo,&userLogin.conInfo,sizeof(userLogin.conInfo)); //更新老结构体，以备将来再用
        if(millis() - userLogin.time0wifi >= T4S){ //如果没有错误，那么在等待T4S之后自然能去wifi检测
          userLogin.index = 0;
          userLogin.conState = CON_WIFI_CHK;
        }
      }else if(userLogin.conInfo.conError == 1){
        userLogin.conState = CON_FIX_WIFI_PRINT;
      }else if(userLogin.conInfo.conError == 2){ //任何其他包括userLogin.conInfo.conError = 1 的各种其他链接错误都当作 conError = 1 处理
        userLogin.conState = CON_FIX_BLYNK_PRINT;
      }else {
        userLogin.conInfo.conError = 3; //即非0,1,2那么EEPROM肯定第一次使用需要全面初始化
        userLogin.conState = CON_FIX_WIFI_PRINT;
      }
    break;
    
    case CON_FIX_WIFI_PRINT: //串口输入提醒
      if(userLogin.conInfo.conError == 1 || userLogin.conInfo.conError == 3){                  //没有找到wifi
        Serial.print("请输入wifi名(");
        Serial.print(userLogin.conInfo.ssid);
        Serial.println(")");
        userLogin.conState = CON_FIX_WIFI_SSID;                    //准备前往串口输入
        userLogin.index = 0;                 //index是输入字节的索引
      }
    break;
    
    case CON_FIX_WIFI_SSID: //串口输入wifi ssid
      if(Serial.available()){
        char c = Serial.read();
        userLogin.conInfo.ssid[userLogin.index] = c;
        userLogin.index++;
        if( c == '\n') {
          c = '\0';
          userLogin.conInfo.ssid[userLogin.index-1] = c;
          userLogin.index = 0;             //用完index后重新回0,为其他输入做准备
          userLogin.conState = CON_FIX_WIFI_PASS;                //for Debug
          userLogin.infoInput = true;      //得到wifi名输入，系统认为目前处于输入各类参数阶段infoInput=true
          Serial.print("请修改wifi密码(");
          Serial.print(userLogin.conInfo.pass);
          Serial.println(")");
        }
      }else{ //添加这块以是的在没有修改性输入的时候，能迅速调到wifi常规检测。目的是在wifi路由器离线时产品能继续尝试连接wifi。
        if(millis() - userLogin.time0wifi >= T4S){
          userLogin.index = 0;
          userLogin.conState = CON_WIFI_CHK;
        }
      }
    break;
    
    case CON_FIX_WIFI_PASS: //串口输入wifi pass
      if(Serial.available()){
        char c = Serial.read();
        userLogin.conInfo.pass[userLogin.index] = c;
        userLogin.index++;
        if( c == '\n'){
          c = '\0';
          userLogin.conInfo.pass[userLogin.index-1] = c;         
          if(millis() - userLogin.time0wifi >= T4S){
            userLogin.index = 0;             //用完index后重新回0,为其他输入做准备 
            userLogin.conState = CON_WIFI_CHK;
            userLogin.wifiErrorCount=0;      //只有输入后去连接，我们才把wifiErrorCount清零.
            Serial.println("WIFI连接中"); 
          }    
        }
      }    
    break;
    
    case CON_FIX_BLYNK_PRINT: //blynk 服务器输入提醒
      if(userLogin.conInfo.conError == 2 || userLogin.conInfo.conError == 3) {
        Serial.print("请输入Blynk服务器IP(");
        Serial.print(userLogin.conInfo.serverIP);
        Serial.println(")");
        userLogin.conState = CON_FIX_BLYNK_IP;                    //准备前往串口输入
        userLogin.index = 0;                 //index是输入字节的索引        
      }    
    break;
    
    case CON_FIX_BLYNK_IP: //串口输入Blynk服务器IP
      if(Serial.available()){
        char c = Serial.read();
        userLogin.conInfo.serverIP[userLogin.index] = c;
        userLogin.index++;
        if( c == '\n') {
          c = '\0';
          userLogin.conInfo.serverIP[userLogin.index-1] = c;
          userLogin.index = 0;             //用完index后重新回0,为其他输入做准备
          userLogin.conState = CON_FIX_BLYNK_PORT;
          userLogin.infoInput = true;      //得到blynk ip输入，系统认为目前处于输入各类参数阶段infoInput=true                
          Serial.print("请输入服务器端口(");
          Serial.print(userLogin.conInfo.serverPort);
          Serial.println(")");
        }
      }else{ 
        /*添加这块的目的是在没有输入连接信息阶段，在T10S后.系统迅速跳到blynk常规检测。
        如果在系统信息输入阶段，那么就跳过等待。*/
        if(millis() - userLogin.time0blynk >= T10S || userLogin.infoInput == true){  //bug?
          userLogin.index = 0;
          userLogin.conState = CON_BLYNK_CHK;
        }    
      }    
    break;
    
    case CON_FIX_BLYNK_PORT: //串口输入Blynk服务器端口
      if(Serial.available()){
        char c = Serial.read();
        userLogin.conInfo.serverPort[userLogin.index] = c;
        userLogin.index++;
        if( c == '\n') {
          c = '\0';
          userLogin.conInfo.serverPort[userLogin.index-1] = c;
          userLogin.index = 0;             //用完index后重新回0,为其他输入做准备
          userLogin.conState = CON_FIX_BLYNK_TOKEN;
          Serial.print("找到手机APP--Blynk下对应设备，请输入设备令牌号(");
          Serial.print(userLogin.conInfo.auth);
          Serial.println(")");                
        }        
      }
    break;
    
    case CON_FIX_BLYNK_TOKEN: //串口输入Blynk服务器令牌auth token
      if(Serial.available()){
        char c = Serial.read();
        userLogin.conInfo.auth[userLogin.index] = c;
        userLogin.index++;
        if( c == '\n') {
          c = '\0';
          userLogin.conInfo.auth[userLogin.index-1] = c;
          userLogin.index = 0;             //用完index后重新回0,为其他输入做准备
          userLogin.infoInput = false;     //令牌输入完成标志着系统参数输入阶段的结束，以后都会是常规连线阶段了
          if(millis() - userLogin.time0blynk >= T10S){
            userLogin.conState = CON_BLYNK_CHK;
            Serial.println("服务器连接中");
          }            
        }        
      }
    break;
    
    case CON_WIFI_CHK:   //检测wifi链接状况
        userLogin.time0wifi = millis(); 
        if(WiFi.status() != WL_CONNECTED){         
          if(userLogin.wifiErrorCount > 0){  //如果重连又失败这个连接已经没救了，必须返回1等待重新输入wifi 帐号密码
            Serial.println("WIFI账号或密码错误");
            userLogin.conInfo.conError = 1;
            userLogin.conState = CON_FIX_GUID;
            break;
          }
          userLogin.wifiErrorCount++; //每次重连失败，计数器就加一
          WiFi.begin(userLogin.conInfo.ssid, userLogin.conInfo.pass); //这个函数的执行时间是120ms左右，但是wifi连接建立需要4S时间
          userLogin.conState = CON_FIX_WIFI_SSID;  //直接跳到wifi输入的地方，因为这里处理的都是wifi账号密码输入后的连接等待，不宜提示出错
        }else{
          if(millis() - userLogin.time0blynk >= T10S || userLogin.infoInput==true) //bug?
            userLogin.conState = CON_BLYNK_CHK; //wifi 成功连接 然后进入检查blynk服务器状况
        }
    break;
    
    case CON_BLYNK_CHK: //完成且每10秒监控一次连接
        userLogin.time0blynk = millis();
        if(Blynk.connected()){
          userLogin.conInfo.conError = 0;
          userLogin.conState = CON_FIX_GUID;   //链接成功了，但是常规检查要从wifi检测开始
          Serial.println("产品连线成功!\n");
        }else{
          //Serial.print("正连接Blynk服务器，输入数据后请耐心等待\n");
          Blynk.config(userLogin.conInfo.auth, userLogin.conInfo.serverIP, atoi(userLogin.conInfo.serverPort));
          Blynk.connect(); //如果连不上，这里要阻塞10秒，就算能连上也会毫秒级耗时
          if(!Blynk.connected()){
            userLogin.conInfo.conError = 2; //报告blynk链接错误
            userLogin.conState = CON_FIX_GUID;
            //Serial.println("blynk信息错误");
          }
        }  
    break;
  }

  if(DEBUG){
    if(userLogin.preConState != userLogin.conState){
      Serial.print("状态: "); 
      Serial.println(userLogin.conState);
      Serial.print("输入中:");
      Serial.println(userLogin.infoInput);
    }
  }
}

#endif
